diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py
index 2b34c3f524..a0c393f692 100644
--- a/lib/matplotlib/contour.py
+++ b/lib/matplotlib/contour.py
@@ -21,6 +21,8 @@ import matplotlib.text as text
 import matplotlib.cbook as cbook
 import matplotlib.patches as mpatches
 import matplotlib.transforms as mtransforms
+from matplotlib.figure import SubFigure
+from matplotlib.tight_layout import get_renderer
 
 
 # We can't use a single line collection for contour because a line
@@ -251,13 +253,20 @@ class ContourLabeler:
     def _get_nth_label_width(self, nth):
         """Return the width of the *nth* label, in pixels."""
         fig = self.axes.figure
-        return (
-            text.Text(0, 0,
-                      self.get_text(self.labelLevelList[nth], self.labelFmt),
-                      figure=fig,
-                      size=self.labelFontSizeList[nth],
-                      fontproperties=self.labelFontProps)
-            .get_window_extent(mpl._tight_layout.get_renderer(fig)).width)
+        # Check if the figure is a SubFigure and get the renderer from the parent figure if so
+        if isinstance(fig, SubFigure):
+            renderer = fig.parent._cachedRenderer
+        else:
+            renderer = get_renderer(fig)
+
+        # Use the renderer to get the window extent
+        window_extent = text.Text(0, 0,
+                                  self.get_text(self.labelLevelList[nth], self.labelFmt),
+                                  figure=fig,
+                                  size=self.labelFontSizeList[nth],
+                                  fontproperties=self.labelFontProps
+                                 ).get_window_extent(renderer).width
+        return window_extent
 
     @_api.deprecated("3.5")
     def get_label_width(self, lev, fmt, fsize):
@@ -267,7 +276,7 @@ class ContourLabeler:
         fig = self.axes.figure
         width = (text.Text(0, 0, lev, figure=fig,
                            size=fsize, fontproperties=self.labelFontProps)
-                 .get_window_extent(mpl._tight_layout.get_renderer(fig)).width)
+                 .get_window_extent(get_renderer(fig)).width)
         width *= 72 / fig.dpi
         return width
 
@@ -1565,6 +1574,187 @@ class QuadContourSet(ContourSet):
         return np.meshgrid(x, y)
 
 
+@_docstring.dedent_interpd
+class QuadContourSet(ContourSet):
+    """
+    Create and store a set of contour lines or filled regions.
+
+    This class is typically not instantiated directly by the user but by
+    `~.Axes.contour` and `~.Axes.contourf`.
+
+    %(contour_set_attributes)s
+    """
+
+    def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs):
+        """
+        Process args and kwargs.
+        """
+        if isinstance(args[0], QuadContourSet):
+            if self.levels is None:
+                self.levels = args[0].levels
+            self.zmin = args[0].zmin
+            self.zmax = args[0].zmax
+            self._corner_mask = args[0]._corner_mask
+            contour_generator = args[0]._contour_generator
+            self._mins = args[0]._mins
+            self._maxs = args[0]._maxs
+            self._algorithm = args[0]._algorithm
+        else:
+            import contourpy
+
+            if algorithm is None:
+                algorithm = mpl.rcParams['contour.algorithm']
+            mpl.rcParams.validate["contour.algorithm"](algorithm)
+            self._algorithm = algorithm
+
+            if corner_mask is None:
+                if self._algorithm == "mpl2005":
+                    # mpl2005 does not support corner_mask=True so if not
+                    # specifically requested then disable it.
+                    corner_mask = False
+                else:
+                    corner_mask = mpl.rcParams['contour.corner_mask']
+            self._corner_mask = corner_mask
+
+            x, y, z = self._contour_args(args, kwargs)
+
+            contour_generator = contourpy.contour_generator(
+                x, y, z, name=self._algorithm, corner_mask=self._corner_mask,
+                line_type=contourpy.LineType.SeparateCode,
+                fill_type=contourpy.FillType.OuterCode,
+                chunk_size=self.nchunk)
+
+            t = self.get_transform()
+
+            # if the transform is not trans data, and some part of it
+            # contains transData, transform the xs and ys to data coordinates
+            if (t != self.axes.transData and
+                    any(t.contains_branch_seperately(self.axes.transData))):
+                trans_to_data = t - self.axes.transData
+                pts = np.vstack([x.flat, y.flat]).T
+                transformed_pts = trans_to_data.transform(pts)
+                x = transformed_pts[..., 0]
+                y = transformed_pts[..., 1]
+
+            self._mins = [ma.min(x), ma.min(y)]
+            self._maxs = [ma.max(x), ma.max(y)]
+
+        self._contour_generator = contour_generator
+
+        return kwargs
+
+    def _contour_args(self, args, kwargs):
+        if self.filled:
+            fn = 'contourf'
+        else:
+            fn = 'contour'
+        Nargs = len(args)
+        if Nargs <= 2:
+            z = ma.asarray(args[0], dtype=np.float64)
+            x, y = self._initialize_x_y(z)
+            args = args[1:]
+        elif Nargs <= 4:
+            x, y, z = self._check_xyz(args[:3], kwargs)
+            args = args[3:]
+        else:
+            raise TypeError("Too many arguments to %s; see help(%s)" %
+                            (fn, fn))
+        z = ma.masked_invalid(z, copy=False)
+        self.zmax = float(z.max())
+        self.zmin = float(z.min())
+        if self.logscale and self.zmin <= 0:
+            z = ma.masked_where(z <= 0, z)
+            _api.warn_external('Log scale: values of z <= 0 have been masked')
+            self.zmin = float(z.min())
+        self._process_contour_level_args(args)
+        return (x, y, z)
+
+    def _check_xyz(self, args, kwargs):
+        """
+        Check that the shapes of the input arrays match; if x and y are 1D,
+        convert them to 2D using meshgrid.
+        """
+        x, y = args[:2]
+        x, y = self.axes._process_unit_info([("x", x), ("y", y)], kwargs)
+
+        x = np.asarray(x, dtype=np.float64)
+        y = np.asarray(y, dtype=np.float64)
+        z = ma.asarray(args[2], dtype=np.float64)
+
+        if z.ndim != 2:
+            raise TypeError(f"Input z must be 2D, not {z.ndim}D")
+        if z.shape[0] < 2 or z.shape[1] < 2:
+            raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
+                            f"but has shape {z.shape}")
+        Ny, Nx = z.shape
+
+        if x.ndim != y.ndim:
+            raise TypeError(f"Number of dimensions of x ({x.ndim}) and y "
+                            f"({y.ndim}) do not match")
+        if x.ndim == 1:
+            nx, = x.shape
+            ny, = y.shape
+            if nx != Nx:
+                raise TypeError(f"Length of x ({nx}) must match number of "
+                                f"columns in z ({Nx})")
+            if ny != Ny:
+                raise TypeError(f"Length of y ({ny}) must match number of "
+                                f"rows in z ({Ny})")
+            x, y = np.meshgrid(x, y)
+        elif x.ndim == 2:
+            if x.shape != z.shape:
+                raise TypeError(
+                    f"Shapes of x {x.shape} and z {z.shape} do not match")
+            if y.shape != z.shape:
+                raise TypeError(
+                    f"Shapes of y {y.shape} and z {z.shape} do not match")
+        else:
+            raise TypeError(f"Inputs x and y must be 1D or 2D, not {x.ndim}D")
+
+        return x, y, z
+
+    def _initialize_x_y(self, z):
+        """
+        Return X, Y arrays such that contour(Z) will match imshow(Z)
+        if origin is not None.
+        The center of pixel Z[i, j] depends on origin:
+        if origin is None, x = j, y = i;
+        if origin is 'lower', x = j + 0.5, y = i + 0.5;
+        if origin is 'upper', x = j + 0.5, y = Nrows - i - 0.5
+        If extent is not None, x and y will be scaled to match,
+        as in imshow.
+        If origin is None and extent is not None, then extent
+        will give the minimum and maximum values of x and y.
+        """
+        if z.ndim != 2:
+            raise TypeError(f"Input z must be 2D, not {z.ndim}D")
+        elif z.shape[0] < 2 or z.shape[1] < 2:
+            raise TypeError(f"Input z must be at least a (2, 2) shaped array, "
+                            f"but has shape {z.shape}")
+        else:
+            Ny, Nx = z.shape
+        if self.origin is None:  # Not for image-matching.
+            if self.extent is None:
+                return np.meshgrid(np.arange(Nx), np.arange(Ny))
+            else:
+                x0, x1, y0, y1 = self.extent
+                x = np.linspace(x0, x1, Nx)
+                y = np.linspace(y0, y1, Ny)
+                return np.meshgrid(x, y)
+        # Match image behavior:
+        if self.extent is None:
+            x0, x1, y0, y1 = (0, Nx, 0, Ny)
+        else:
+            x0, x1, y0, y1 = self.extent
+        dx = (x1 - x0) / Nx
+        dy = (y1 - y0) / Ny
+        x = x0 + (np.arange(Nx) + 0.5) * dx
+        y = y0 + (np.arange(Ny) + 0.5) * dy
+        if self.origin == 'upper':
+            y = y[::-1]
+        return np.meshgrid(x, y)
+
+
 _docstring.interpd.update(contour_doc="""
 `.contour` and `.contourf` draw contour lines and filled contours,
 respectively.  Except as noted, function signatures and return values
